feat(automation): add Linux systemd user-timer scheduler#29
Open
vsilv wants to merge 1 commit into
Open
Conversation
Scheduled automation (setup, `automation`, and config-driven reconcile) previously worked only on macOS: the composition root hard-coded the launchd adapter and the domain model leaked launchd vocabulary (`plist_path`). This adds Linux support behind the existing `SchedulerAdapter` port. - Generalize the automation domain model: `plist_path` becomes `manifest_path`, and the manifest location is computed per platform (launchd plist on macOS, a systemd `.timer` under the user unit directory on Linux, honoring `XDG_CONFIG_HOME` when it is an absolute path). - Add `SystemdSchedulerAdapter`: writes a `.service` + `.timer` unit pair, enables and (re)starts the timer through `systemctl --user`, and reads status back from `systemctl show`. `OnActiveSec=0` mirrors launchd `RunAtLoad`; `OnUnitActiveSec` carries the interval. `is_loaded` gates on `ActiveState`, so a stopped or disabled timer is not reported as loaded. The `systemctl` runner is injectable so the adapter is unit-testable. - Select the scheduler adapter by platform in the composition root via `default_scheduler_adapter()`; the explicit adapter-injection path is unchanged. - Provider-neutral `automation status` output (`manifest` / `scheduler loaded`). - Split the shared process-error helper into `scheduler/process.py` so neither adapter depends on the other. - Docs: README support matrix and the automation architecture notes. Note: `automation status` (human and `--json`) now reports `manifest_path` instead of the launchd-specific `plist_path`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Scheduled automation (
setup,automation, and config-driven reconcile) onlyworks on macOS. The composition root hard-codes
LaunchdSchedulerAdapter, andthe automation domain model leaks launchd vocabulary (
plist_path). On Linux,setup/automationfail because there is no scheduler adapter andlaunchctlis absent.
What this does
Adds Linux support behind the existing
SchedulerAdapterport, using systemd--usertimers — the closest analogue to launchd LaunchAgents (per-user,persistent, and rich enough to fill the existing
ScheduledJobStatusmodel).default_scheduler_adapter()returnsthe systemd adapter on Linux and the launchd adapter elsewhere, wired into the
composition root (
app.py). The explicitadapters.schedulerinjection pathis unchanged, so tests still inject fakes.
plist_pathbecomesmanifest_path, and themanifest location is computed per platform: a launchd plist under
~/Library/LaunchAgentson macOS, a systemd.timerunder the user unitdirectory on Linux (honoring
XDG_CONFIG_HOMEwhen it is an absolute path).The macOS-only PATH fallbacks no longer leak onto Linux.
SystemdSchedulerAdapter. Writes a.service+.timerunit pair,enables and (re)starts the timer through
systemctl --user, and reads statusback from
systemctl show.OnActiveSec=0mirrors launchdRunAtLoad;OnUnitActiveSeccarries the interval.is_loadedgates onActiveState, soa stopped or disabled timer is not reported as loaded. The
systemctlrunneris injectable so the adapter is unit-testable without touching the real user
manager.
scheduler/process.pysoneither adapter depends on the other.
automation statusoutput and updated docs (README supportmatrix, automation architecture notes).
Rejected alternative: cron
cronwas considered and rejected.crontabexposes no per-job status, runcount, exit code, or PID, so
ScheduledJobStatuswould be half-empty and behavedifferently from macOS. systemd
showmaps cleanly onto the existing model.Behavior / contract change
automation status(human output and--json) now reportsmanifest_pathinstead of the launchd-specific
plist_path; the human labels aremanifestand
scheduler loaded. This is a deliberate, provider-neutral rename.Two systemd choices are intentional and documented in the plan:
Persistent=trueis omitted — persystemd.timer(5)it only affectsOnCalendar=timers, so it would be inert config on a monotonic timer.run_countis not reported on Linux —NRestartscountsRestart=restarts,always 0 for a
oneshotservice, so reporting it would be misleading. macOSkeeps launchd's
runs.Testing
uv run pytest— 477 passed.uv run ruff check .— clean.uv build— ok.automation task through the CLI installed a real timer;
automation statusreported it
installed,scheduler loaded: yes, with the correct intervaland state; the scheduled run fired and exited 0;
is_loadedcorrectlyreturned false after the timer was stopped; uninstall removed both unit files
and left no residual units in the user manager.